package com.google.gwt.gwtpages.client.page.loader;

import java.util.HashMap;
import java.util.Iterator;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
import com.google.gwt.gwtpages.client.message.exceptions.PageNotFoundException;
import com.google.gwt.gwtpages.client.page.PageAttributes;
import com.google.gwt.gwtpages.client.page.LoadedPageContainer;
import com.google.gwt.gwtpages.client.page.Page;

/**
 * Page loaded that uses a separate async request to lazy-load each page when
 * requested
 * 
 * @author Joe Hudson
 * 
 */
public abstract class AsyncPageLoader implements PageLoader {

	private HashMap<String, Boolean> loadedPageFlag = new HashMap<String, Boolean>();
	private HashMap<String, Page> newPages = new HashMap<String, Page>();  // if we have loaded them but they have not been requested
	private HashMap<String, PageCreator> pages = new HashMap<String, AsyncPageLoader.PageCreator>();
	private HashMap<String, PageAttributes> metaData = new HashMap<String, PageAttributes>();
	private boolean loadPagesIndividually;

	/**
	 * All pages registered with this loader will use a single async call to retrieve
	 */
	public AsyncPageLoader() {
	}

	/**
	 * All pages registered with this loader will use their own async call to retrieve (1 call for every page to load)
	 */
	public AsyncPageLoader(boolean loadPagesIndividually) {
		this.loadPagesIndividually = loadPagesIndividually;
	}

	public boolean isValidPageToken(String token) {
		return (null != pages.get(token));
	}

	public Iterator<String> getValidPageTokens() {
		return pages.keySet().iterator();
	}

	/**
	 * Register a page
	 * 
	 * @param token
	 *            the page token
	 * @param pageLoader
	 *            the {@link PageCreator}
	 */
	public AsyncPageLoader registerPage(String token, PageCreator pageLoader) {
		return registerPage(token, pageLoader, null);
	}

	/**
	 * 
	 * Register a page
	 * 
	 * @param token
	 *            the page token
	 * @param pageLoader
	 *            the {@link PageCreator}
	 * @param metaData
	 *            the {@link PageAttributes}
	 */
	public AsyncPageLoader registerPage(String token, PageCreator pageLoader,
			PageAttributes metaData) {
		pages.put(token, pageLoader);
		if (null != metaData)
			this.metaData.put(token, metaData);
		return this;
	}

	public void getPage(final String pageToken,
			final PageLoadCallback pageLoadCallback) {
		// has the page been loaded?
		if (null != loadedPageFlag.get(pageToken)) {
			// don't do an async load
			if (null != newPages.get(pageToken)) {
				// have we created the page during a async load for a differnet page
				try {
					pageLoadCallback.onPageFound(new LoadedPageContainer(pageToken,
							newPages.get(pageToken), getPageAttributes(pageToken),
							AsyncPageLoader.this));
				}
				catch (PageNotFoundException e) {
					// we know the page is here
					pageLoadCallback.onPageFound(new LoadedPageContainer(pageToken,
							newPages.get(pageToken), null,
							AsyncPageLoader.this));
				}
				newPages.remove(pageToken);
			}
			else {
				// create a new page
				PageCreator creator = pages.get(pageToken);
				try {
					pageLoadCallback.onPageFound(new LoadedPageContainer(pageToken,
							creator.createPage(), getPageAttributes(pageToken),
							AsyncPageLoader.this));
				}
				catch (PageNotFoundException e) {
					// we know the page is here
					pageLoadCallback.onPageFound(new LoadedPageContainer(pageToken,
							creator.createPage(), null,
							AsyncPageLoader.this));
				}
			}

		}

		// do an async load
		final PageCreator loader = pages.get(pageToken);
		if (null != loader) {
			if (loadPagesIndividually) {
				// only load a single page
				GWT.runAsync(new RunAsyncCallback() {
					public void onSuccess() {
						try {
							pageLoadCallback.onPageFound(new LoadedPageContainer(pageToken,
									loader.createPage(), getPageAttributes(pageToken),
									AsyncPageLoader.this));
							loadedPageFlag.put(pageToken, Boolean.TRUE);
						} catch (Throwable e) {
							pageLoadCallback.onPageLoadFailure(pageToken, e);
						}
					}
	
					public void onFailure(Throwable reason) {
						pageLoadCallback.onPageLoadFailure(pageToken, reason);
					}
				});
			}
			else {
				// load all pages at once
				GWT.runAsync(new RunAsyncCallback() {
					public void onSuccess() {
						Throwable pageException = null;
						LoadedPageContainer rtn = null;
						for (String key : pages.keySet()) {
							try {
								Page page = loader.createPage();
								loadedPageFlag.put(pageToken, Boolean.TRUE);
								if (key.equals(pageToken)) {
									// this is the page we are looking for
									rtn = new LoadedPageContainer(pageToken,
											page, getPageAttributes(pageToken),
											AsyncPageLoader.this);
								}
								else {
									newPages.put(pageToken, page);
								}
							}
							catch (Throwable e) {
								if (key.equals(pageToken)) {
									pageException = e;
								}
							}
						}
						if (null != rtn) {
							pageLoadCallback.onPageFound(rtn);
						}
						else {
							pageLoadCallback.onPageLoadFailure(pageToken, pageException);
						}
					}
	
					public void onFailure(Throwable reason) {
						pageLoadCallback.onPageLoadFailure(pageToken, reason);
					}
				});
			}
		} else {
			// the page wasn't found
			pageLoadCallback.onPageNotFound(pageToken);
		}
	}

	/**
	 * Interface class used to define a class used to return the desired page
	 * instance. This is used so the actual page doesn't need to be associated
	 * with the page token so the page can be loaded asynchronously.
	 * 
	 * @author Joe Hudson
	 */
	public interface PageCreator {
		
		/**
		 * Return a new instance of a {@link Page}
		 */
		public Page createPage();
	}
}